/*! \file kru_fw.c
	Main testing framework implementation file.
*/

#include <assert.h>
#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include "kru.h"
#include "_kru.h"

const char *SuiteSetUpFailed = "Failure";

/*
        This single static makes the whole system non-reentrant
 */
/*@shared@*/ /*@null@*/ static kru_test *runningTest = NULL;

/*
	Assertion reporting
 */

static void reportAssert(kru_test *self, int condition, int abortTest, /*@null@*/ const char* file, int line, /*@null@*/ const char* message)
{
	if(0 != condition)
	{
		return;
	}
	self->suite->registry->format(" %s", 0 != abortTest ? "Fatal" : "Failure");
	if(NULL != file)
	{
		self->suite->registry->format(", %s:%d", file, line);
	}
	if(NULL != message)
	{
		self->suite->registry->format(", %s", message);
	}
}

void kru_assert(int condition, int abortTest, const char* file, int line, const char* message)
{
	assert(NULL != runningTest);

	runningTest->asserts++;
	reportAssert(runningTest, condition, abortTest, file, line, message);
	if(0 != condition)
	{
		runningTest->successes++;
	}
	else
	{
		runningTest->failures++;
		if(0 != abortTest)
		{
			longjmp(runningTest->jbuf, 1);
		}
	}
}

/*
        Create a new test
 */
static /*@notnull@*/ kru_test* kru_test_new(const char* name, kru_test_setup_proc setup, kru_test_fixture test, kru_test_teardown_proc teardown)
{
	kru_test *self;
	self = kru_calloc(1, sizeof(kru_test));
/*@-mustfreeonly@*/
	self->name = dupstring(name);
/*@+mustfreeonly@*/
	self->setup = setup;
	self->test = test;
	self->teardown = teardown;
	return self;
}

/*
	Delete the test
 */
static void kru_test_delete(/*@only@*/ kru_test *self)
{
	free(self->name);
	free(self);
}

/*
	Run the test
	Performs optional set up & tear down
	Note setjmp/longjmp calls
 */
static int kru_test_run(/*@shared@*/ kru_test *self)
{
	self->suite->registry->format("\tRunning test");
	if(NULL != self->name)
	{
		self->suite->registry->format(" \'%s\'", self->name);
	}
	self->suite->registry->format("... ");

	runningTest = self;

	if(NULL != self->setup)
	{
		self->setups++;
		if(0 != self->setup(self->suite->testData))
			self->setupSuccesses++;
		else
			self->setupFailures++;
	}
	if(0 == setjmp(self->jbuf))
	{
		self->runs++;
		self->start = clock();
		self->test(self->suite->testData);
		self->stop = clock();
	}
	else
	{
		self->stop = clock();
	}
	if(NULL != self->teardown)
	{
		self->teardowns++;
		if(0 != self->teardown(self->suite->testData))
			self->teardownSuccesses++;
		else
			self->teardownFailures++;
	}

	runningTest = NULL;
	self->suite->registry->format("\n");

	return 0 == self->failures;
}

/*
	Create a new test suite
 */
static /*@notnull@*/ kru_suite* kru_suite_new(const char *name, kru_suite_setup_proc setup, kru_suite_teardown_proc teardown)
{
	kru_suite *self;

	self = kru_calloc(1, sizeof(kru_suite));
/*@-mustfreeonly@*/
	self->name = dupstring(name);
/*@+mustfreeonly@*/
	self->setup = setup;
	self->teardown = teardown;
	return self;
}

/*
	Delete the test suite
	Also deletes all the tests in the suite
 */
static void kru_suite_delete(/*@only@*/ kru_suite *self)
{
	kru_test *current, *previous;

	current = self->head;
	while(NULL != current)
	{
		previous = current;
		current = current->next;
		kru_test_delete(previous);
	}
	free(self->name);
	free(self);
}

/*
	Adds a test to a test suite
 */
static void kru_suite_add_test(/*@shared@*/ kru_suite *self, /*@shared@*/ kru_test *test)
{
	test->next = NULL;
	if(NULL == self->head)
	{
		self->head = test;
	}
	if(NULL != self->tail)
	{
		self->tail->next = test;
	}
	self->tail = test;
	test->suite = self;
}

kru_test* kru_suite_add_new_test(kru_suite *self, const char* name, kru_test_setup_proc setup, kru_test_fixture test, kru_test_teardown_proc teardown)
{
	/*@shared@*/ kru_test *newTest;

	assert(NULL != self);
	assert(NULL != test);

	newTest = kru_test_new(name, setup, test, teardown);
	kru_suite_add_test(self, newTest);
	return newTest;
}

/*
	Run the test suite
 */
static void kru_suite_run(kru_suite *self)
{
	kru_test *current;
	int result;

	result = 1; // True
	self->registry->format("Running test suite");
	if(NULL != self->name)
	{
		self->registry->format(" \'%s\'", self->name);
	}
	self->registry->format("...\n");
	if(NULL != self->setup)
	{
		self->setups++;
		self->testData = self->setup();
		if(SuiteSetUpFailed != self->testData)
			self->setupSuccesses++;
		else
			self->setupFailures++;
	}

	if(NULL != self->head)
		self->runs++;
	current = self->head;
	while(NULL != current)
	{
		result &= kru_test_run(current); // NB: any test failure->suite failure
		current = current->next;
	}
	if(0 != result)
	{
		self->successes++;
	}
	else
	{
		self->failures++;
	}

	if(NULL != self->teardown)
	{
		self->teardowns++;
		if(0 != self->teardown(self->testData))
			self->teardownSuccesses++;
		else
			self->teardownFailures++;
	}
	self->registry->format("\n");
}

int kru_default_format(const char *format, ...)
{
	va_list args;
	int result;

	va_start(args, format);
	result = vfprintf(stderr, format, args);
	va_end(args);
	return result;
}

kru_registry* kru_registry_new(const char *name)
{
	kru_registry *self;

	self = kru_calloc(1, sizeof(kru_registry));
/*@-mustfreeonly@*/
	self->name = dupstring(name);
/*@+mustfreeonly@*/
	self->format = kru_default_format;
	return self;
}

void kru_registry_delete(/*@only@*/ kru_registry *self)
{
	kru_suite *current, *previous;

	assert(NULL != self);

	current = self->head;
	while(NULL != current)
	{
		previous = current;
		current = current->next;
		kru_suite_delete(previous);
	}
	free(self->name);
	free(self);
}

kru_format_proc kru_registry_set_format_proc(kru_registry *self, kru_format_proc format_proc)
{
	kru_format_proc result;

	assert(NULL != self);
	assert(NULL != format_proc);

	result = self->format;
	self->format = format_proc;
	return result;
}

/*
	Add a suite to a registry
 */
static void kru_registry_add_suite(kru_registry *self, kru_suite *suite)
{
	suite->next = NULL;
	if(NULL == self->head)
		self->head = suite;
	if(NULL != self->tail)
		self->tail->next = suite;
	self->tail = suite;
	suite->registry = self;
}

kru_suite* kru_registry_add_new_suite(kru_registry *self, const char *name, kru_suite_setup_proc setup, kru_suite_teardown_proc teardown)
{
	kru_suite *newSuite;

	assert(NULL != self);

	newSuite = kru_suite_new(name, setup, teardown);
	kru_registry_add_suite(self, newSuite);
	return newSuite;
}

/*
	Signals; used to intercept certain possible errors in tests
 */

static void sigFloatingPointError(/*@unused@*/ int signo)
{
	assert(NULL != runningTest);
	reportAssert(runningTest, 0, 1, NULL, -1, "floating point error");
	longjmp(runningTest->jbuf, 2);
}

static void sigIllegalInstruction(/*@unused@*/ int signo)
{
	assert(NULL != runningTest);
	reportAssert(runningTest, 0, 1, NULL, -1, "illegal instruction");
	longjmp(runningTest->jbuf, 2);
}

static void sigPageFault(/*@unused@*/ int signo)
{
	assert(NULL != runningTest);
	reportAssert(runningTest, 0, 1, NULL, -1, "page fault");
	longjmp(runningTest->jbuf, 2);
}

void kru_registry_run(kru_registry *self)
{
	kru_suite *current;

	assert(NULL != self);

	(void)signal(SIGFPE, sigFloatingPointError);
	(void)signal(SIGILL, sigIllegalInstruction);
	(void)signal(SIGSEGV, sigPageFault);
	self->format("Running ");
	if(NULL != self->name)
	{
		self->format("\'%s\' ", self->name);
	}
	self->format("test set...\n");
	current = self->head;
	while(NULL != current)
	{
		kru_suite_run(current);
		current = current->next;
	}
	self->format("Test set ");
	if(NULL != self->name)
	{
		self->format("\'%s\' ", self->name);
	}
	self->format("run complete.\n");
	(void)signal( SIGFPE, SIG_DFL );
	(void)signal( SIGILL, SIG_DFL );
	(void)signal( SIGSEGV, SIG_DFL );
}
